/* * Copyright 2013 mpowers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.trsst; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.text.SimpleDateFormat; import java.util.Date; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import org.bouncycastle.crypto.BufferedBlockCipher; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.engines.IESEngine; import org.bouncycastle.crypto.generators.KDF2BytesGenerator; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.IESCipher; /** * Shared utilities to try to keep the cryptography implementation in one place * for easier review. * * @author mpowers */ public class Crypto { public static byte[] encryptKeyWithIES(byte[] input, long entryId, PublicKey publicKey, PrivateKey privateKey) throws GeneralSecurityException { try { // BC appears to be happier with BCECPublicKeys: // see BC's IESCipher.engineInit's check for ECPublicKey publicKey = new BCECPublicKey((ECPublicKey) publicKey, null); return _cryptIES(input, publicKey, true); } catch (GeneralSecurityException e) { log.error("Error while encrypting key", e); throw e; } } public static byte[] decryptKeyWithIES(byte[] input, long entryId, PublicKey publicKey, PrivateKey privateKey) throws GeneralSecurityException { try { // BC appears to be happier with BCECPrivateKeys: privateKey = new BCECPrivateKey((ECPrivateKey) privateKey, null); return _cryptIES(input, privateKey, false); } catch (GeneralSecurityException e) { log.error("Error while decrypting key", e); throw new GeneralSecurityException(e); } } private static byte[] _cryptIES(byte[] input, Key recipient, boolean forEncryption) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException { IESCipher cipher = new IESCipher(new IESEngine( new ECDHBasicAgreement(), new KDF2BytesGenerator( new SHA1Digest()), new HMac(new SHA256Digest()), new PaddedBufferedBlockCipher(new CBCBlockCipher( new AESEngine())))); cipher.engineInit(forEncryption ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, recipient, new SecureRandom()); return cipher.engineDoFinal(input, 0, input.length); } public static byte[] generateAESKey() { byte[] result = new byte[32]; new SecureRandom().nextBytes(result); return result; } public static byte[] encryptAES(byte[] input, byte[] key) throws InvalidCipherTextException { return _cryptBytesAES(input, key, true); } public static byte[] decryptAES(byte[] input, byte[] key) throws InvalidCipherTextException { return _cryptBytesAES(input, key, false); } // h/t Steve Weis, Michael Rogers, and liberationtech private static byte[] _cryptBytesAES(byte[] input, byte[] key, boolean forEncryption) throws InvalidCipherTextException { assert key.length == 32; // 32 bytes == 256 bits return process(input, new PaddedBufferedBlockCipher(new CBCBlockCipher( new AESEngine())), new KeyParameter(key), forEncryption); // note: using zero IV because we generate a new key for every message } // h/t Adam Paynter http://stackoverflow.com/users/41619/ private static byte[] process(byte[] input, BufferedBlockCipher bufferedBlockCipher, CipherParameters cipherParameters, boolean forEncryption) throws InvalidCipherTextException { bufferedBlockCipher.init(forEncryption, cipherParameters); int inputOffset = 0; int inputLength = input.length; int maximumOutputLength = bufferedBlockCipher .getOutputSize(inputLength); byte[] output = new byte[maximumOutputLength]; int outputOffset = 0; int outputLength = 0; int bytesProcessed; bytesProcessed = bufferedBlockCipher.processBytes(input, inputOffset, inputLength, output, outputOffset); outputOffset += bytesProcessed; outputLength += bytesProcessed; bytesProcessed = bufferedBlockCipher.doFinal(output, outputOffset); outputOffset += bytesProcessed; outputLength += bytesProcessed; if (outputLength == output.length) { return output; } else { byte[] truncatedOutput = new byte[outputLength]; System.arraycopy(output, 0, truncatedOutput, 0, outputLength); return truncatedOutput; } } /** * Computes hashcash proof-of-work stamp for the given input and * bitstrength. Servers can choose which bitstrength they accept, but we * recommend at least 20. The colon ":" is a delimiter in hashcash so we * replace all occurances in a token with ".". * * This machine is calculating stamps at a mean rate of 340ms, 694ms, * 1989ms, 4098ms, and 6563ms for bits of 19, 20, 21, 22, and 23 * respectively. * * @param bitstrength * number of leading zero bits to find * @param timestamp * the timestamp/entry-id of the enclosing entry * @param token * a feed-id or mention-id or tag * @return */ public static final String computeStamp(int bitstrength, long timestamp, String token) { try { if (token.indexOf(':') != -1) { token = token.replace(":", "."); } String formattedDate = new SimpleDateFormat("YYMMdd") .format(new Date(timestamp)); String prefix = "1:" + Integer.toString(bitstrength) + ":" + formattedDate + ":" + token + "::" + Long.toHexString(timestamp) + ":"; int masklength = bitstrength / 8; byte[] prefixBytes = prefix.getBytes("UTF-8"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); int i; int b; byte[] hash; long counter = 0; while (true) { sha1.update(prefixBytes); sha1.update(Long.toHexString(counter).getBytes()); hash = sha1.digest(); // 20 bytes long for (i = 0; i < 20; i++) { b = (i < masklength) ? 0 : 255 >> (bitstrength % 8); if (b != (b | hash[i])) { // no match; keep trying break; } if (i == masklength) { // we're a match: return the stamp // System.out.println(Common.toHex(hash)); return prefix + Long.toHexString(counter); } } counter++; // keep going forever until we find it } } catch (UnsupportedEncodingException e) { log.error("No string encoding found: ", e); } catch (NoSuchAlgorithmException e) { log.error("No hash algorithm found: ", e); } log.error("Exiting without stamp: should never happen"); return null; } /** * Verifies the specified hashcash proof-of-work stamp for the given * timestamp and token. * * @return true if verified, false if failed or invalid. */ public static final boolean verifyStamp(String stamp, long timestamp, String token) { String[] fields = stamp.split(":"); if (fields.length != 7) { log.info("verifyStamp: invalid number of fields: " + fields.length); return false; } if (!"1".equals(fields[0])) { log.info("verifyStamp: invalid version: " + fields[0]); return false; } int bitstrength; try { bitstrength = Integer.parseInt(fields[1]); } catch (NumberFormatException e) { log.info("verifyStamp: invalid bit strength: " + fields[1]); return false; } String formattedDate = new SimpleDateFormat("YYMMdd").format(new Date( timestamp)); if (!formattedDate.equals(fields[2])) { log.info("verifyStamp: invalid date: " + fields[2]); return false; } if (!token.equals(fields[3])) { log.info("verifyStamp: invalid token: " + fields[3]); return false; } // other fields are ignored; // now verify hash: try { int b; byte[] hash = MessageDigest.getInstance("SHA-1").digest( stamp.getBytes("UTF-8")); for (int i = 0; i < 20; i++) { b = (i < bitstrength / 8) ? 0 : 255 >> (bitstrength % 8); if (b != (b | hash[i])) { return false; } if (i == bitstrength / 8) { // stamp is verified return true; } } } catch (UnsupportedEncodingException e) { log.error("No string encoding found: ", e); } catch (NoSuchAlgorithmException e) { log.error("No hash algorithm found: ", e); } return false; } private final static org.slf4j.Logger log = org.slf4j.LoggerFactory .getLogger(Crypto.class); }